package org.andengine.entity.modifier;

import android.util.FloatMath;

import org.andengine.entity.IEntity;
import org.andengine.util.modifier.IModifier;
import org.andengine.util.modifier.SequenceModifier;
import org.andengine.util.modifier.SequenceModifier.ISubSequenceModifierListener;
import org.andengine.util.modifier.ease.EaseLinear;
import org.andengine.util.modifier.ease.IEaseFunction;

/**
 * (c) 2010 Nicolas Gramlich
 * (c) 2011 Zynga Inc.
 *
 * @author Nicolas Gramlich
 * @since 16:50:02 - 16.06.2010
 */
public class PathModifier extends EntityModifier {
    // ===========================================================
    // Constants
    // ===========================================================

    // ===========================================================
    // Fields
    // ===========================================================

    private final SequenceModifier<IEntity> mSequenceModifier;

    private IPathModifierListener mPathModifierListener;

    private final Path mPath;

    // ===========================================================
    // Constructors
    // ===========================================================

    public PathModifier(final float pDuration, final Path pPath) {
        this(pDuration, pPath, null, null, EaseLinear.getInstance());
    }

    public PathModifier(final float pDuration, final Path pPath, final IEaseFunction pEaseFunction) {
        this(pDuration, pPath, null, null, pEaseFunction);
    }

    public PathModifier(final float pDuration, final Path pPath, final IEntityModifierListener pEntityModiferListener) {
        this(pDuration, pPath, pEntityModiferListener, null, EaseLinear.getInstance());
    }

    public PathModifier(final float pDuration, final Path pPath, final IPathModifierListener pPathModifierListener) {
        this(pDuration, pPath, null, pPathModifierListener, EaseLinear.getInstance());
    }

    public PathModifier(final float pDuration, final Path pPath, final IPathModifierListener pPathModifierListener, final IEaseFunction pEaseFunction) {
        this(pDuration, pPath, null, pPathModifierListener, pEaseFunction);
    }

    public PathModifier(final float pDuration, final Path pPath, final IEntityModifierListener pEntityModiferListener, final IEaseFunction pEaseFunction) {
        this(pDuration, pPath, pEntityModiferListener, null, pEaseFunction);
    }

    public PathModifier(final float pDuration, final Path pPath, final IEntityModifierListener pEntityModiferListener, final IPathModifierListener pPathModifierListener) throws IllegalArgumentException {
        this(pDuration, pPath, pEntityModiferListener, pPathModifierListener, EaseLinear.getInstance());
    }

    public PathModifier(final float pDuration, final Path pPath, final IEntityModifierListener pEntityModiferListener, final IPathModifierListener pPathModifierListener, final IEaseFunction pEaseFunction) throws IllegalArgumentException {
        super(pEntityModiferListener);
        final int pathSize = pPath.getSize();

        if (pathSize < 2) {
            throw new IllegalArgumentException("Path needs at least 2 waypoints!");
        }

        this.mPath = pPath;
        this.mPathModifierListener = pPathModifierListener;

        final MoveModifier[] moveModifiers = new MoveModifier[pathSize - 1];

        final float[] coordinatesX = pPath.getCoordinatesX();
        final float[] coordinatesY = pPath.getCoordinatesY();

        final float velocity = pPath.getLength() / pDuration;

        final int modifierCount = moveModifiers.length;
        for (int i = 0; i < modifierCount; i++) {
            final float duration = pPath.getSegmentLength(i) / velocity;
            moveModifiers[i] = new MoveModifier(duration, coordinatesX[i], coordinatesX[i + 1], coordinatesY[i], coordinatesY[i + 1], null, pEaseFunction);
        }

		/* Create a new SequenceModifier and register the listeners that
         * call through to mEntityModifierListener and mPathModifierListener. */
        this.mSequenceModifier = new SequenceModifier<IEntity>(
                new ISubSequenceModifierListener<IEntity>() {
                    @Override
                    public void onSubSequenceStarted(final IModifier<IEntity> pModifier, final IEntity pEntity, final int pIndex) {
                        if (PathModifier.this.mPathModifierListener != null) {
                            PathModifier.this.mPathModifierListener.onPathWaypointStarted(PathModifier.this, pEntity, pIndex);
                        }
                    }

                    @Override
                    public void onSubSequenceFinished(final IModifier<IEntity> pEntityModifier, final IEntity pEntity, final int pIndex) {
                        if (PathModifier.this.mPathModifierListener != null) {
                            PathModifier.this.mPathModifierListener.onPathWaypointFinished(PathModifier.this, pEntity, pIndex);
                        }
                    }
                },
                new IEntityModifierListener() {
                    @Override
                    public void onModifierStarted(final IModifier<IEntity> pModifier, final IEntity pEntity) {
                        PathModifier.this.onModifierStarted(pEntity);
                        if (PathModifier.this.mPathModifierListener != null) {
                            PathModifier.this.mPathModifierListener.onPathStarted(PathModifier.this, pEntity);
                        }
                    }

                    @Override
                    public void onModifierFinished(final IModifier<IEntity> pEntityModifier, final IEntity pEntity) {
                        PathModifier.this.onModifierFinished(pEntity);
                        if (PathModifier.this.mPathModifierListener != null) {
                            PathModifier.this.mPathModifierListener.onPathFinished(PathModifier.this, pEntity);
                        }
                    }
                },
                moveModifiers
        );
    }

    protected PathModifier(final PathModifier pPathModifier) throws DeepCopyNotSupportedException {
        this.mPath = pPathModifier.mPath.deepCopy();
        this.mSequenceModifier = pPathModifier.mSequenceModifier.deepCopy();
    }

    @Override
    public PathModifier deepCopy() throws DeepCopyNotSupportedException {
        return new PathModifier(this);
    }

    // ===========================================================
    // Getter & Setter
    // ===========================================================

    public Path getPath() {
        return this.mPath;
    }

    // ===========================================================
    // Methods for/from SuperClass/Interfaces
    // ===========================================================

    @Override
    public boolean isFinished() {
        return this.mSequenceModifier.isFinished();
    }

    @Override
    public float getSecondsElapsed() {
        return this.mSequenceModifier.getSecondsElapsed();
    }

    @Override
    public float getDuration() {
        return this.mSequenceModifier.getDuration();
    }

    public IPathModifierListener getPathModifierListener() {
        return this.mPathModifierListener;
    }

    public void setPathModifierListener(final IPathModifierListener pPathModifierListener) {
        this.mPathModifierListener = pPathModifierListener;
    }

    @Override
    public void reset() {
        this.mSequenceModifier.reset();
    }

    @Override
    public float onUpdate(final float pSecondsElapsed, final IEntity pEntity) {
        return this.mSequenceModifier.onUpdate(pSecondsElapsed, pEntity);
    }

    // ===========================================================
    // Methods
    // ===========================================================

    // ===========================================================
    // Inner and Anonymous Classes
    // ===========================================================

    public static interface IPathModifierListener {
        // ===========================================================
        // Constants
        // ===========================================================

        // ===========================================================
        // Fields
        // ===========================================================

        public void onPathStarted(final PathModifier pPathModifier, final IEntity pEntity);

        public void onPathWaypointStarted(final PathModifier pPathModifier, final IEntity pEntity, final int pWaypointIndex);

        public void onPathWaypointFinished(final PathModifier pPathModifier, final IEntity pEntity, final int pWaypointIndex);

        public void onPathFinished(final PathModifier pPathModifier, final IEntity pEntity);
    }

    public static class Path {
        // ===========================================================
        // Constants
        // ===========================================================

        // ===========================================================
        // Fields
        // ===========================================================

        private final float[] mXs;
        private final float[] mYs;

        private int mIndex;
        private boolean mLengthChanged = false;
        private float mLength;

        // ===========================================================
        // Constructors
        // ===========================================================

        public Path(final int pLength) {
            this.mXs = new float[pLength];
            this.mYs = new float[pLength];

            this.mIndex = 0;
            this.mLengthChanged = false;
        }

        public Path(final float[] pCoordinatesX, final float[] pCoordinatesY) throws IllegalArgumentException {
            if (pCoordinatesX.length != pCoordinatesY.length) {
                throw new IllegalArgumentException("Coordinate-Arrays must have the same length.");
            }

            this.mXs = pCoordinatesX;
            this.mYs = pCoordinatesY;

            this.mIndex = pCoordinatesX.length;
            this.mLengthChanged = true;
        }

        public Path(final Path pPath) {
            final int size = pPath.getSize();
            this.mXs = new float[size];
            this.mYs = new float[size];

            System.arraycopy(pPath.mXs, 0, this.mXs, 0, size);
            System.arraycopy(pPath.mYs, 0, this.mYs, 0, size);

            this.mIndex = pPath.mIndex;
            this.mLengthChanged = pPath.mLengthChanged;
            this.mLength = pPath.mLength;
        }

        public Path deepCopy() {
            return new Path(this);
        }

        // ===========================================================
        // Getter & Setter
        // ===========================================================

        public Path to(final float pX, final float pY) {
            this.mXs[this.mIndex] = pX;
            this.mYs[this.mIndex] = pY;

            this.mIndex++;

            this.mLengthChanged = true;

            return this;
        }

        public float[] getCoordinatesX() {
            return this.mXs;
        }

        public float[] getCoordinatesY() {
            return this.mYs;
        }

        public int getSize() {
            return this.mXs.length;
        }

        public float getLength() {
            if (this.mLengthChanged) {
                this.updateLength();
            }
            return this.mLength;
        }

        public float getSegmentLength(final int pSegmentIndex) {
            final float[] coordinatesX = this.mXs;
            final float[] coordinatesY = this.mYs;

            final int nextSegmentIndex = pSegmentIndex + 1;

            final float dx = coordinatesX[pSegmentIndex] - coordinatesX[nextSegmentIndex];
            final float dy = coordinatesY[pSegmentIndex] - coordinatesY[nextSegmentIndex];

            return FloatMath.sqrt(dx * dx + dy * dy);
        }

        // ===========================================================
        // Methods for/from SuperClass/Interfaces
        // ===========================================================

        // ===========================================================
        // Methods
        // ===========================================================

        private void updateLength() {
            float length = 0.0f;

            for (int i = this.mIndex - 2; i >= 0; i--) {
                length += this.getSegmentLength(i);
            }
            this.mLength = length;
        }

        // ===========================================================
        // Inner and Anonymous Classes
        // ===========================================================
    }
}
